﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Media.Animation;
using System.Windows.Markup;
using System.Windows.Controls.Primitives;

namespace UmlControlLibrary
{
    public class LineHelper
    {
        public enum Side
        {
            Top,
            Bottom,
            Left,
            Right
        }
        public enum LineDirection { Horizontal, Vertical };
        public static Point Interpolate(Point p1, Point p2, double factor)
        {
            return new Point(p1.X + (p2.X - p1.X) * factor, p1.Y + (p2.Y - p1.Y) * factor);
        }

        public static PolyLineSegment GetOrthogonalSegment(Point p1, Point pc1, Point pc2, Point p2, Side side1, Side side2)
        {
            LineDirection dirP1 = GetDirection(side1);
            LineDirection dirP2 = GetDirection(side2);
            Point pc = LineHelper.Interpolate(pc1, pc2, 0.5);
            PointCollection pointCol = new PointCollection();
            if (dirP1 == LineHelper.LineDirection.Horizontal)
                pointCol.Add(new Point(pc.X, p1.Y));
            else
                pointCol.Add(new Point(p1.X, pc.Y));

            pointCol.Add(pc);

            if (dirP2 == LineHelper.LineDirection.Horizontal)
                pointCol.Add(new Point(pc.X, p2.Y));
            else
                pointCol.Add(new Point(p2.X, pc.Y));

            pointCol.Add(p2);

            return new PolyLineSegment(pointCol, true);
        }
        public static LineDirection GetDirection(Side side)
        {
            return ((side == Side.Left) || (side == Side.Right)) ? LineDirection.Horizontal : LineDirection.Vertical;
        }
    }


    public class UmlRelationControl : UmlBaseControl
    {
        const int HalfThumbSize = 3;
        const int ThumbSize = 6;

        private DoubleCollection strokes = new DoubleCollection();

        public UmlRelationType RelationType
        {
            get { return (UmlRelationType)GetValue(RelationTypeProperty); }
            set { 
                SetValue(RelationTypeProperty, value);
                SetRelationType(value);
            }
        }

        public UmlRelationEndControl End1Control
        {
            get;
            set;
        }
        public UmlRelationEndControl End2Control
        {
            get;
            set;
        }
        public DraggableLabel LabelControl
        {
            get;
            set;
        }


        public List<Point> RelationLinePoints
        {
            get;
            set;
        }

        PathFigure _figure;
        public PathFigure LineFigure
        {
            get
            {
                return _figure;
            }
            set
            {
                if (Geometry.Figures.Contains(_figure))
                    Geometry.Figures[Geometry.Figures.IndexOf(_figure)] = value;
                else
                    Geometry.Figures.Add(value);
                _figure = value;
                _figure.IsFilled = false;
            }
        }
        public PathGeometry Geometry
        {
            get;
            set;
        }

        public Path RelationGraphPath
        {
            get;
            set;
        }

        public Path TrackingGraphPath
        {
            get;
            set;
        }
        
        public Point GetCenter(Control e)
        {
            double l = Canvas.GetLeft(e),
                   r = Canvas.GetRight(e),
                   t = Canvas.GetTop(e),
                   b = Canvas.GetBottom(e);
            if (double.IsNaN(r))
            {
                r = l + e.ActualWidth;
                b = t + e.ActualHeight;
            }
            return new Point((l + r) / 2, (t + b) / 2);
        }

        private double getMagnitude(Point l1, Point l2)
        {
            double x, y;
            x = l2.X - l1.X; x *= x;
            y = l2.Y - l1.Y; y *= y;
            return x + y;
        }

        private double DistanceFromLine(Point p, Point l1, Point l2)
        {
            const double EPS = 0.000001;
            const double EPSEPS = EPS * EPS;
            double SqLineMag, u, ix, iy;
            double result;

            SqLineMag = getMagnitude(l1, l2);
            if (SqLineMag < EPSEPS)
                return -1;

            u = ((p.X - l1.X) * (l2.X - l1.X) + (p.Y - l1.Y) * (l2.Y - l1.Y)) / SqLineMag;
            if ((u < EPS) || (u > 1))
            {
                ix = getMagnitude(p, l1);
                iy = getMagnitude(p, l2);
                result = Math.Min(ix, iy);
            }
            else
            {
                ix = l1.X + u * (l2.X - l1.X);
                iy = l1.Y + u * (l2.Y - l1.Y);
                result = getMagnitude(p, new Point(ix, iy));
            }
            return Math.Sqrt(result);
        }

        public bool IsPointOnSegment(Point p, Point p1, Point p2, out double factor, double tolerance, double maxDist)
        {
            if (p == p1)
            {
                factor = 0;
                return true;
            }
            if (p == p2)
            {
                factor = 1;
                return true;
            }

            Vector v1 = p2 - p1;
            Vector v2 = p - p1;
            Vector v1n = v1; v1n.Normalize();
            Vector v2n = v2; v2n.Normalize();

            double dot = v1n.X * v2n.X + v1n.Y * v2n.Y;
            if ((dot >= 1 - tolerance) && (dot <= 1 + tolerance))
            {
                factor = v2.Length / v1.Length;
                if ((factor >= 0) && (factor <= 1) && (DistanceFromLine(p, p1, p2) < maxDist))
                    return true;
            }
            factor = 0;
            return false;
        }

        public int GetSegmentPart(Point p, out double factor)
        {
            PolyLineSegment pls = (PolyLineSegment)LineFigure.Segments[0];
            factor = 0;
            for (int i = 0; i < pls.Points.Count - 1; i++)
            {
                Point p1 = pls.Points[i];
                Point p2 = pls.Points[i + 1];

                if (IsPointOnSegment(p, p1, p2, out factor, 0.001, 2))
                    return i;
            }
            return -1;
        }

        protected void SetLabelPos(Point p)
        {
            if (LabelControl != null)
            {
                Canvas.SetLeft(LabelControl, p.X - LabelControl.ActualWidth / 2);
                Canvas.SetTop(LabelControl, p.Y + 5);
            }
        }

        public void InitRelationLink()
        {
            Point c1, c2;
            PolyLineSegment pls;

            LineFigure.Segments.Clear();

            c2 = End2Control.EndControl.MagnetPoint(GetCenter(End1Control.EndControl));
            c1 = End1Control.EndControl.MagnetPoint(c2);

            LineHelper.Side side;
            Point p1 = End1Control.EndControl.MagnetPoint(c1, out side);
            End1Control.Side = side;
            Point p2 = End2Control.EndControl.MagnetPoint(c2, out side);
            End2Control.Side = side;

            LineFigure.StartPoint = p1;

            PointCollection pointCol = new PointCollection();
            pointCol.Add(p1);
            pointCol.Add(p2);
            pls = new PolyLineSegment(pointCol, true);
            LineFigure.Segments.Add(pls);
        }

        public void RecalcRelationLink()
        {
            RecalcRelationLink(null);
        }

        public void RecalcRelationLink(UmlControl refreshEnd)
        {
            Point c1, c2;

            // initial relation calculation
            if (LineFigure.Segments.Count == 0)
                InitRelationLink();
            PointCollection pointCol = (LineFigure.Segments[0] as PolyLineSegment).Points;
            c1 = pointCol[1];
            c2 = pointCol[pointCol.Count - 2];

            if (pointCol.Count == 2)
            {
                c1 = End2Control.EndControl.MagnetPoint(c1);
                c2 = End1Control.EndControl.MagnetPoint(c2);
            }

            LineHelper.Side side;
            Point p1 = End1Control.EndControl.MagnetPoint(c1, out side);
            End1Control.Side = side;
            Point p2 = End2Control.EndControl.MagnetPoint(c2, out side);
            End2Control.Side = side;

            LineFigure.StartPoint = p1;

            pointCol[0] = p1;
            pointCol[pointCol.Count - 1] = p2;

            if (End1Control.Arrow != null)
            {
                End1Control.Arrow.UpdateArrow(pointCol[1], p1);
                LineFigure.StartPoint = End1Control.Arrow.P2;
                End1Control.RefreshLabel();
            }
            if (End2Control.Arrow != null)
            {
                End2Control.Arrow.UpdateArrow(pointCol[pointCol.Count - 2], p2);
                pointCol[pointCol.Count - 1] = End2Control.Arrow.P2;
                End2Control.RefreshLabel();
            }

            Point center = LineHelper.Interpolate(p1, p2, 0.5);

            SetLabelPos(center);

            if (thumbs.Count > 0)
            {
                Canvas.SetLeft(thumbs[0], p1.X - HalfThumbSize);
                Canvas.SetTop(thumbs[0], p1.Y - HalfThumbSize);
                Canvas.SetLeft(thumbs[thumbs.Count - 1], p2.X - HalfThumbSize);
                Canvas.SetTop(thumbs[thumbs.Count - 1], p2.Y - HalfThumbSize);
            }
        }

        public void SetRelationType(UmlRelationType relationType)
        {
            if (LineFigure.Segments.Count == 0)
                InitRelationLink();

            PolyLineSegment seg = (LineFigure.Segments[0] as PolyLineSegment);
            Point s1 = seg.Points[0],
                  s2 = LineFigure.StartPoint,
                  e1 = seg.Points[seg.Points.Count - 2],
                  e2 = seg.Points[seg.Points.Count - 1];

            switch (relationType)
            {
                case UmlRelationType.Association:
                    End1Control.Arrow = new UmlArrow(End1Control.Label, s1, s2);
                    End2Control.Arrow = new UmlArrow(End2Control.Label, e1, e2);
                    RelationGraphPath.Fill = Brushes.Black;
                    break;
                case UmlRelationType.Inheritance:
                    End1Control.Arrow = new UmlArrow(End1Control.Label, s1, s2);
                    End2Control.Arrow = new UmlArrowInheritance(End2Control.Label, e1, e2);
                    RelationGraphPath.Fill = Brushes.White;
                    break;
                case UmlRelationType.Aggregate:
                    End1Control.Arrow = new UmlArrow(End1Control.Label, s1, s2);
                    End2Control.Arrow = new UmlArrowDiamond(End2Control.Label, e1, e2);
                    RelationGraphPath.Fill = Brushes.White;
                    break;
                case UmlRelationType.Composition:
                    End1Control.Arrow = new UmlArrow(End1Control.Label, s1, s2);
                    End2Control.Arrow = new UmlArrowDiamond(End2Control.Label, e1, e2);
                    RelationGraphPath.Fill = Brushes.Black;
                    break;
                case UmlRelationType.Note:
                    RelationGraphPath.Fill = Brushes.Black;
                    strokes.Add(10);strokes.Add(5);
                    RelationGraphPath.StrokeDashArray = strokes.Clone();
                    break;
            }

            RecalcRelationLink();
        }

        private void AddControlPoint(int segment, double factor)
        {
            PointCollection pointCol = (LineFigure.Segments[0] as PolyLineSegment).Points;
            Point p = LineHelper.Interpolate(pointCol[segment], pointCol[segment + 1], factor);
            pointCol.Insert(segment + 1, p);
            if (Selected)
            {
                Thumb t = AddThumb(p);
                thumbs.Insert(segment + 1, t);
                OwnerElement.AddChild(t);
            }
        }

        void t_DragDelta(object sender, DragDeltaEventArgs e)
        {
            int count = thumbs.Count;

            Thumb item = sender as Thumb;
            int index = thumbs.IndexOf(item);
            if ((index > 0) && (index < count - 1))
            {
                Point p;
                PointCollection controlPoints = (LineFigure.Segments[0] as PolyLineSegment).Points;
                p = controlPoints[index];

                double X = SelectableItemControl.AlignToGrid(p.X + e.HorizontalChange);
                double Y = SelectableItemControl.AlignToGrid(p.Y + e.VerticalChange);

                Canvas.SetLeft(item, X - HalfThumbSize);
                Canvas.SetTop(item, Y - HalfThumbSize);

                controlPoints[index] = new Point(X, Y);
                RecalcRelationLink();
            }
            IsDragging = true;
            e.Handled = true;
        }

        private Thumb AddThumb(Point p)
        {
            Thumb t = new Thumb();
            t.Height = ThumbSize;
            t.Width = ThumbSize;
            t.BorderBrush = Brushes.Blue;
            t.BorderThickness = new Thickness(HalfThumbSize);
            Canvas.SetLeft(t, p.X - HalfThumbSize);
            Canvas.SetTop(t, p.Y - HalfThumbSize);
            t.DragDelta += new DragDeltaEventHandler(t_DragDelta);

            //t.PreviewMouseLeftButtonDown += new System.Windows.Input.MouseButtonEventHandler(t_PreviewMouseDown);
            //t.PreviewMouseUp += new System.Windows.Input.MouseButtonEventHandler(t_PreviewMouseUp);
            t.PreviewMouseLeftButtonUp += new MouseButtonEventHandler(t_PreviewMouseLeftButtonUp);

            return t;
        }

        void t_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            if (IsDragging)
            {
                Thumb t = sender as Thumb;
                for (int index = thumbs.Count - 2; index > 0; index--)
                {
                    PolyLineSegment pls = (PolyLineSegment)LineFigure.Segments[0];
                    Point p1 = pls.Points[index - 1];
                    Point p2 = pls.Points[index + 1];
                    Point p = pls.Points[index];
                    double factor = 0;
                    if (IsPointOnSegment(p, p1, p2, out factor, 0.001, 10))
                    {
                        pls.Points.RemoveAt(index);
                        t = thumbs[index];
                        this.OwnerElement.RemoveChild(t);
                        thumbs.RemoveAt(index);
                    }
                }
            }
            IsDragging = false;
        }

        bool IsDragging = false;

        List<Thumb> thumbs = new List<Thumb>();

        private void CleanThumbs()
        {
            foreach (Thumb t in thumbs)
                this.OwnerElement.RemoveChild(t);
            thumbs.Clear();
        }

        private void Hilight(bool hilight)
        {
            if (hilight)
            {
                this.RelationGraphPath.StrokeThickness = 2;
                this.RelationGraphPath.Stroke = Brushes.Gray;
            }
            else
            {
                this.RelationGraphPath.StrokeThickness = 1;
                this.RelationGraphPath.Stroke = Brushes.Black;
            }

            if (this.RelationGraphPath.StrokeDashArray != null)
            {
                this.RelationGraphPath.StrokeDashArray = strokes.Clone();
                for (int i = 0; i < this.strokes.Count; i++)
                    this.RelationGraphPath.StrokeDashArray[i] /= this.RelationGraphPath.StrokeThickness;
            }
        }

        public override void Select()
        {
            base.Select();
            CleanThumbs();

            foreach (Point p in (LineFigure.Segments[0] as PolyLineSegment).Points)
                thumbs.Add(AddThumb(p));
            foreach (Thumb t in thumbs)
                this.OwnerElement.AddChild(t);
            Hilight(true);
        }

        public override void Deselect()
        {
            base.Deselect();
            CleanThumbs();
            Hilight(false);
        }

//        public static DependencyProperty LabelProperty = DependencyProperty.Register("Label", typeof(string), typeof(UmlRelation));
        public static DependencyProperty RelationTypeProperty;// = DependencyProperty.Register("RelationType", typeof(UmlRelationType), typeof(UmlRelation));

        static UmlRelationControl()
        {
            FrameworkPropertyMetadata relationTypeMetadata = new FrameworkPropertyMetadata(
                delegate(DependencyObject p, DependencyPropertyChangedEventArgs args)
                {
                    (p as UmlRelationControl).SetRelationType((UmlRelationType)args.NewValue );
                });

            relationTypeMetadata.BindsTwoWayByDefault = false;
            relationTypeMetadata.DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;

            RelationTypeProperty = DependencyProperty.Register("RelationType",
                typeof(UmlRelationType),
                typeof(UmlRelationControl),
                relationTypeMetadata);
        }

        public UmlRelationControl(UmlRelation relation)
            : base()
        {
            this.Geometry = new PathGeometry();
            LineFigure = new PathFigure();
            this.End1Control = new UmlRelationEndControl(this.Geometry, relation.End1);
            this.End2Control = new UmlRelationEndControl(this.Geometry, relation.End2);
            LabelControl = new DraggableLabel();

            if (!this.End1Control.EndControl.Relations.Contains(this))
                this.End1Control.EndControl.Relations.Add(this);

            if (!this.End2Control.EndControl.Relations.Contains(this))
                this.End2Control.EndControl.Relations.Add(this);

            this.RelationGraphPath = new Path();
            this.RelationGraphPath.Stroke = Brushes.Black;
            this.RelationGraphPath.Fill = Brushes.Black;
            this.RelationGraphPath.DataContext = this;
            this.RelationGraphPath.SetBinding(Path.DataProperty, "Geometry");

            this.TrackingGraphPath = new Path();
            this.TrackingGraphPath.Stroke = Brushes.Transparent;
            this.TrackingGraphPath.Fill = Brushes.Transparent;
            this.TrackingGraphPath.DataContext = this;
            this.TrackingGraphPath.SetBinding(Path.DataProperty, "Geometry");
            this.TrackingGraphPath.StrokeThickness = 5.0;

            this.DataContext = relation;

            Binding binding = new Binding("RelationType");
            binding.Mode = BindingMode.TwoWay;
            binding.Source = this.DataContext;
            BindingOperations.SetBinding(this, UmlRelationControl.RelationTypeProperty, binding);

            binding = new Binding("Name");
            binding.Mode = BindingMode.TwoWay;
            binding.Source = this.DataContext;
            BindingOperations.SetBinding(LabelControl, DraggableLabel.TextProperty, binding);

            this.SetRelationType(relation.RelationType);

            this.OwnerElement = End1Control.EndControl.OwnerElement;
            //this.OwnerElement = (UmlDiagramControl)UmlModel.CurrentDiagram.DiagramControls[UmlElementList.Instance.GetElementByGuid(relation.End1.EndElement.Guid)].OwnerElement;
            this.OwnerElement.AddChild(this.RelationGraphPath);
            this.OwnerElement.AddChild(this.TrackingGraphPath);
            this.OwnerElement.AddChild(this.End1Control.LabelControl);
            this.OwnerElement.AddChild(this.End2Control.LabelControl);
            this.OwnerElement.AddChild(this.LabelControl);

            this.TrackingGraphPath.MouseLeftButtonDown += new MouseButtonEventHandler(UmlRelation_MouseLeftButtonDown);
            this.End1Control.LabelControl.MouseLeftButtonDown += new MouseButtonEventHandler(UmlRelation_MouseLeftButtonDown);
            this.End2Control.LabelControl.MouseLeftButtonDown += new MouseButtonEventHandler(UmlRelation_MouseLeftButtonDown);
            this.LabelControl.MouseLeftButtonDown += new MouseButtonEventHandler(UmlRelation_MouseLeftButtonDown);
            this.TrackingGraphPath.MouseEnter += new MouseEventHandler(RelationGraphPath_MouseEnter);
            this.TrackingGraphPath.MouseLeave += new MouseEventHandler(RelationGraphPath_MouseLeave);
        }

        void RelationGraphPath_MouseLeave(object sender, MouseEventArgs e)
        {
            if (!Selected)
                Hilight(false);
        }

        void RelationGraphPath_MouseEnter(object sender, MouseEventArgs e)
        {
            Hilight(true);
        }

        void UmlRelation_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            if (!this.Selected)
                Select();
            else
            {
                // find if we clicked in a part of the relation line
                double factor;
                Point mousePos;
                mousePos = this.OwnerElement.GetMousePos(e);
                int SegmentPart = GetSegmentPart(mousePos, out factor);
                if (SegmentPart >= 0)
                    AddControlPoint(SegmentPart, factor);
            }
            e.Handled = true;
        }
    }

}
